package schema_test

import (
	"database/sql"
	"reflect"
	"sync"
	"testing"
	"time"

	"gorm.io/gorm"
	"gorm.io/gorm/schema"
	"gorm.io/gorm/utils/tests"
)

func TestFieldValuerAndSetter(t *testing.T) {
	var (
		userSchema, _ = schema.Parse(&tests.User{}, &sync.Map{}, schema.NamingStrategy{})
		user          = tests.User{
			Model: gorm.Model{
				ID:        10,
				CreatedAt: time.Now(),
				UpdatedAt: time.Now(),
				DeletedAt: gorm.DeletedAt{Time: time.Now(), Valid: true},
			},
			Name:     "valuer_and_setter",
			Age:      18,
			Birthday: tests.Now(),
			Active:   true,
		}
		reflectValue = reflect.ValueOf(&user)
	)

	// test valuer
	values := map[string]interface{}{
		"name":       user.Name,
		"id":         user.ID,
		"created_at": user.CreatedAt,
		"updated_at": user.UpdatedAt,
		"deleted_at": user.DeletedAt,
		"age":        user.Age,
		"birthday":   user.Birthday,
		"active":     true,
	}
	checkField(t, userSchema, reflectValue, values)

	var f *bool
	// test setter
	newValues := map[string]interface{}{
		"name":       "valuer_and_setter_2",
		"id":         2,
		"created_at": time.Now(),
		"updated_at": nil,
		"deleted_at": time.Now(),
		"age":        20,
		"birthday":   time.Now(),
		"active":     f,
	}

	for k, v := range newValues {
		if err := userSchema.FieldsByDBName[k].Set(reflectValue, v); err != nil {
			t.Errorf("no error should happen when assign value to field %v, but got %v", k, err)
		}
	}
	newValues["updated_at"] = time.Time{}
	newValues["active"] = false
	checkField(t, userSchema, reflectValue, newValues)

	// test valuer and other type
	age := myint(10)
	var nilTime *time.Time
	newValues2 := map[string]interface{}{
		"name":       sql.NullString{String: "valuer_and_setter_3", Valid: true},
		"id":         &sql.NullInt64{Int64: 3, Valid: true},
		"created_at": tests.Now(),
		"updated_at": nilTime,
		"deleted_at": time.Now(),
		"age":        &age,
		"birthday":   mytime(time.Now()),
		"active":     mybool(true),
	}

	for k, v := range newValues2 {
		if err := userSchema.FieldsByDBName[k].Set(reflectValue, v); err != nil {
			t.Errorf("no error should happen when assign value to field %v, but got %v", k, err)
		}
	}
	newValues2["updated_at"] = time.Time{}
	checkField(t, userSchema, reflectValue, newValues2)
}

func TestPointerFieldValuerAndSetter(t *testing.T) {
	var (
		userSchema, _      = schema.Parse(&User{}, &sync.Map{}, schema.NamingStrategy{})
		name               = "pointer_field_valuer_and_setter"
		age           uint = 18
		active             = true
		user               = User{
			Model: &gorm.Model{
				ID:        10,
				CreatedAt: time.Now(),
				DeletedAt: gorm.DeletedAt{Time: time.Now(), Valid: true},
			},
			Name:     &name,
			Age:      &age,
			Birthday: tests.Now(),
			Active:   &active,
		}
		reflectValue = reflect.ValueOf(&user)
	)

	// test valuer
	values := map[string]interface{}{
		"name":       user.Name,
		"id":         user.ID,
		"created_at": user.CreatedAt,
		"deleted_at": user.DeletedAt,
		"age":        user.Age,
		"birthday":   user.Birthday,
		"active":     true,
	}
	checkField(t, userSchema, reflectValue, values)

	// test setter
	newValues := map[string]interface{}{
		"name":       "valuer_and_setter_2",
		"id":         2,
		"created_at": time.Now(),
		"deleted_at": time.Now(),
		"age":        20,
		"birthday":   time.Now(),
		"active":     false,
	}

	for k, v := range newValues {
		if err := userSchema.FieldsByDBName[k].Set(reflectValue, v); err != nil {
			t.Errorf("no error should happen when assign value to field %v, but got %v", k, err)
		}
	}
	checkField(t, userSchema, reflectValue, newValues)

	// test valuer and other type
	age2 := myint(10)
	newValues2 := map[string]interface{}{
		"name":       sql.NullString{String: "valuer_and_setter_3", Valid: true},
		"id":         &sql.NullInt64{Int64: 3, Valid: true},
		"created_at": tests.Now(),
		"deleted_at": time.Now(),
		"age":        &age2,
		"birthday":   mytime(time.Now()),
		"active":     mybool(true),
	}

	for k, v := range newValues2 {
		if err := userSchema.FieldsByDBName[k].Set(reflectValue, v); err != nil {
			t.Errorf("no error should happen when assign value to field %v, but got %v", k, err)
		}
	}
	checkField(t, userSchema, reflectValue, newValues2)
}

func TestAdvancedDataTypeValuerAndSetter(t *testing.T) {
	var (
		userSchema, _ = schema.Parse(&AdvancedDataTypeUser{}, &sync.Map{}, schema.NamingStrategy{})
		name          = "advanced_data_type_valuer_and_setter"
		deletedAt     = mytime(time.Now())
		isAdmin       = mybool(false)
		user          = AdvancedDataTypeUser{
			ID:           sql.NullInt64{Int64: 10, Valid: true},
			Name:         &sql.NullString{String: name, Valid: true},
			Birthday:     sql.NullTime{Time: time.Now(), Valid: true},
			RegisteredAt: mytime(time.Now()),
			DeletedAt:    &deletedAt,
			Active:       mybool(true),
			Admin:        &isAdmin,
		}
		reflectValue = reflect.ValueOf(&user)
	)

	// test valuer
	values := map[string]interface{}{
		"id":            user.ID,
		"name":          user.Name,
		"birthday":      user.Birthday,
		"registered_at": user.RegisteredAt,
		"deleted_at":    user.DeletedAt,
		"active":        user.Active,
		"admin":         user.Admin,
	}
	checkField(t, userSchema, reflectValue, values)

	// test setter
	newDeletedAt := mytime(time.Now())
	newIsAdmin := mybool(true)
	newValues := map[string]interface{}{
		"id":            sql.NullInt64{Int64: 1, Valid: true},
		"name":          &sql.NullString{String: name + "rename", Valid: true},
		"birthday":      time.Now(),
		"registered_at": mytime(time.Now()),
		"deleted_at":    &newDeletedAt,
		"active":        mybool(false),
		"admin":         &newIsAdmin,
	}

	for k, v := range newValues {
		if err := userSchema.FieldsByDBName[k].Set(reflectValue, v); err != nil {
			t.Errorf("no error should happen when assign value to field %v, but got %v", k, err)
		}
	}
	checkField(t, userSchema, reflectValue, newValues)

	newValues2 := map[string]interface{}{
		"id":            5,
		"name":          name + "rename2",
		"birthday":      time.Now(),
		"registered_at": time.Now(),
		"deleted_at":    time.Now(),
		"active":        true,
		"admin":         false,
	}

	for k, v := range newValues2 {
		if err := userSchema.FieldsByDBName[k].Set(reflectValue, v); err != nil {
			t.Errorf("no error should happen when assign value to field %v, but got %v", k, err)
		}
	}
	checkField(t, userSchema, reflectValue, newValues2)
}

type UserWithPermissionControl struct {
	ID    uint
	Name  string `gorm:"-"`
	Name2 string `gorm:"->"`
	Name3 string `gorm:"<-"`
	Name4 string `gorm:"<-:create"`
	Name5 string `gorm:"<-:update"`
	Name6 string `gorm:"<-:create,update"`
	Name7 string `gorm:"->:false;<-:create,update"`
	Name8 string `gorm:"->;-:migration"`
}

func TestParseFieldWithPermission(t *testing.T) {
	user, err := schema.Parse(&UserWithPermissionControl{}, &sync.Map{}, schema.NamingStrategy{})
	if err != nil {
		t.Fatalf("Failed to parse user with permission, got error %v", err)
	}

	fields := []*schema.Field{
		{Name: "ID", DBName: "id", BindNames: []string{"ID"}, DataType: schema.Uint, PrimaryKey: true, Size: 64, Creatable: true, Updatable: true, Readable: true, HasDefaultValue: true, AutoIncrement: true},
		{Name: "Name", DBName: "", BindNames: []string{"Name"}, DataType: "", Tag: `gorm:"-"`, Creatable: false, Updatable: false, Readable: false},
		{Name: "Name2", DBName: "name2", BindNames: []string{"Name2"}, DataType: schema.String, Tag: `gorm:"->"`, Creatable: false, Updatable: false, Readable: true},
		{Name: "Name3", DBName: "name3", BindNames: []string{"Name3"}, DataType: schema.String, Tag: `gorm:"<-"`, Creatable: true, Updatable: true, Readable: true},
		{Name: "Name4", DBName: "name4", BindNames: []string{"Name4"}, DataType: schema.String, Tag: `gorm:"<-:create"`, Creatable: true, Updatable: false, Readable: true},
		{Name: "Name5", DBName: "name5", BindNames: []string{"Name5"}, DataType: schema.String, Tag: `gorm:"<-:update"`, Creatable: false, Updatable: true, Readable: true},
		{Name: "Name6", DBName: "name6", BindNames: []string{"Name6"}, DataType: schema.String, Tag: `gorm:"<-:create,update"`, Creatable: true, Updatable: true, Readable: true},
		{Name: "Name7", DBName: "name7", BindNames: []string{"Name7"}, DataType: schema.String, Tag: `gorm:"->:false;<-:create,update"`, Creatable: true, Updatable: true, Readable: false},
		{Name: "Name8", DBName: "name8", BindNames: []string{"Name8"}, DataType: schema.String, Tag: `gorm:"->;-:migration"`, Creatable: false, Updatable: false, Readable: true, IgnoreMigration: true},
	}

	for _, f := range fields {
		checkSchemaField(t, user, f, func(f *schema.Field) {})
	}
}

type ID int64
type INT int
type INT8 int8
type INT16 int16
type INT32 int32
type INT64 int64
type UINT uint
type UINT8 uint8
type UINT16 uint16
type UINT32 uint32
type UINT64 uint64
type FLOAT32 float32
type FLOAT64 float64
type BOOL bool
type STRING string
type TypeAlias struct {
	ID
	INT     `gorm:"column:fint"`
	INT8    `gorm:"column:fint8"`
	INT16   `gorm:"column:fint16"`
	INT32   `gorm:"column:fint32"`
	INT64   `gorm:"column:fint64"`
	UINT    `gorm:"column:fuint"`
	UINT8   `gorm:"column:fuint8"`
	UINT16  `gorm:"column:fuint16"`
	UINT32  `gorm:"column:fuint32"`
	UINT64  `gorm:"column:fuint64"`
	FLOAT32 `gorm:"column:ffloat32"`
	FLOAT64 `gorm:"column:ffloat64"`
	BOOL    `gorm:"column:fbool"`
	STRING  `gorm:"column:fstring"`
}

func TestTypeAliasField(t *testing.T){
	alias, err := schema.Parse(&TypeAlias{}, &sync.Map{}, schema.NamingStrategy{})
	if err != nil {
		t.Fatalf("Failed to parse TypeAlias with permission, got error %v", err)
	}

	fields := []*schema.Field{
		{Name: "ID",      DBName: "id",       BindNames: []string{"ID"},      DataType: schema.Int   , Creatable: true, Updatable: true, Readable: true, Size: 64, PrimaryKey: true, HasDefaultValue: true, AutoIncrement: true },
		{Name: "INT",     DBName: "fint",     BindNames: []string{"INT"},     DataType: schema.Int   , Creatable: true, Updatable: true, Readable: true, Size: 64, Tag: `gorm:"column:fint"`},
		{Name: "INT8",    DBName: "fint8",    BindNames: []string{"INT8"},    DataType: schema.Int   , Creatable: true, Updatable: true, Readable: true, Size: 8,  Tag: `gorm:"column:fint8"`},
		{Name: "INT16",   DBName: "fint16",   BindNames: []string{"INT16"},   DataType: schema.Int   , Creatable: true, Updatable: true, Readable: true, Size: 16, Tag: `gorm:"column:fint16"`},
		{Name: "INT32",   DBName: "fint32",   BindNames: []string{"INT32"},   DataType: schema.Int   , Creatable: true, Updatable: true, Readable: true, Size: 32, Tag: `gorm:"column:fint32"`},
		{Name: "INT64",   DBName: "fint64",   BindNames: []string{"INT64"},   DataType: schema.Int   , Creatable: true, Updatable: true, Readable: true, Size: 64, Tag: `gorm:"column:fint64"`},
		{Name: "UINT",    DBName: "fuint",    BindNames: []string{"UINT"},    DataType: schema.Uint  , Creatable: true, Updatable: true, Readable: true, Size: 64, Tag: `gorm:"column:fuint"`},
		{Name: "UINT8",   DBName: "fuint8",   BindNames: []string{"UINT8"},   DataType: schema.Uint  , Creatable: true, Updatable: true, Readable: true, Size: 8,  Tag: `gorm:"column:fuint8"`},
		{Name: "UINT16",  DBName: "fuint16",  BindNames: []string{"UINT16"},  DataType: schema.Uint  , Creatable: true, Updatable: true, Readable: true, Size: 16, Tag: `gorm:"column:fuint16"`},
		{Name: "UINT32",  DBName: "fuint32",  BindNames: []string{"UINT32"},  DataType: schema.Uint  , Creatable: true, Updatable: true, Readable: true, Size: 32, Tag: `gorm:"column:fuint32"`},
		{Name: "UINT64",  DBName: "fuint64",  BindNames: []string{"UINT64"},  DataType: schema.Uint  , Creatable: true, Updatable: true, Readable: true, Size: 64, Tag: `gorm:"column:fuint64"`},
		{Name: "FLOAT32", DBName: "ffloat32", BindNames: []string{"FLOAT32"}, DataType: schema.Float , Creatable: true, Updatable: true, Readable: true, Size: 32, Tag: `gorm:"column:ffloat32"`},
		{Name: "FLOAT64", DBName: "ffloat64", BindNames: []string{"FLOAT64"}, DataType: schema.Float , Creatable: true, Updatable: true, Readable: true, Size: 64, Tag: `gorm:"column:ffloat64"`},
		{Name: "BOOL",    DBName: "fbool",    BindNames: []string{"BOOL"},    DataType: schema.Bool  , Creatable: true, Updatable: true, Readable: true, Tag: `gorm:"column:fbool"`},
		{Name: "STRING",  DBName: "fstring",  BindNames: []string{"STRING"},  DataType: schema.String, Creatable: true, Updatable: true, Readable: true, Tag: `gorm:"column:fstring"`},
	}

	for _, f := range fields {
		checkSchemaField(t, alias, f, func(f *schema.Field) {})
	}
}